<!DOCTYPE html>
<html>
<head>
	<title>Browser Dialer</title>
</head>
<body>
	<script>
		"use strict";
		// Enable a much more aggressive JIT for performance gains

		// Copyright (c) 2021 XRAY. Mozilla Public License 2.0.
		let url = "ws://" + window.location.host + "/websocket?token=csrfToken";
		let clientIdleCount = 0;
		let upstreamGetCount = 0;
		let upstreamWsCount = 0;
		let upstreamPostCount = 0;

		function prepareRequestInit(extra) {
			const requestInit = {};
			if (extra.referrer) {
				// note: we have to strip the protocol and host part.
				// Browsers disallow that, and will reset the value to current page if attempted.
				const referrer = URL.parse(extra.referrer);
				requestInit.referrer = referrer.pathname + referrer.search + referrer.hash;
				requestInit.referrerPolicy = "unsafe-url";
			}

			if (extra.headers) {
				requestInit.headers = extra.headers;
			}

			return requestInit;
		}

		let check = function () {
			if (clientIdleCount > 0) {
				return;
			}
			clientIdleCount += 1;
			console.log("Prepare", url);
			let ws = new WebSocket(url);
			// arraybuffer is significantly faster in chrome than default
			// blob, tested with chrome 123
			ws.binaryType = "arraybuffer";
			// note: this event listener is later overwritten after the
			// handshake has completed. do not attempt to modernize it without
			// double-checking that this continues to work
			ws.onmessage = function (event) {
				clientIdleCount -= 1;
				let task = JSON.parse(event.data);
				switch (task.method) {
					case "WS": {
						upstreamWsCount += 1;
						console.log("Dial WS", task.url, task.extra.protocol);
						const wss = new WebSocket(task.url, task.extra.protocol);
						wss.binaryType = "arraybuffer";
						let opened = false;
						ws.onmessage = function (event) {
							wss.send(event.data)
						};
						wss.onopen = function (event) {
							opened = true;
							ws.send("ok")
						};
						wss.onmessage = function (event) {
							ws.send(event.data)
						};
						wss.onclose = function (event) {
							upstreamWsCount -= 1;
							console.log("Dial WS DONE, remaining: ", upstreamWsCount);
							ws.close()
						};
						wss.onerror = function (event) {
							!opened && ws.send("fail")
							wss.close()
						};
						ws.onclose = function (event) {
							wss.close()
						};
						break;
					}
					case "GET": {
						(async () => {
							const requestInit = prepareRequestInit(task.extra);

							console.log("Dial GET", task.url);
							ws.send("ok");
							const controller = new AbortController();

							/*
							Aborting a streaming response in JavaScript
							requires two levers to be pulled:

							First, the streaming read itself has to be cancelled using
							reader.cancel(), only then controller.abort() will actually work.

							If controller.abort() alone is called while a
							reader.read() is ongoing, it will block until the server closes the
							response, the page is refreshed or the network connection is lost.
							*/

							let reader = null;
							ws.onclose = (event) => {
								try {
									reader && reader.cancel();
								} catch(e) {}

								try {
									controller.abort();
								} catch(e) {}
							};

							try {
								upstreamGetCount += 1;

								requestInit.signal = controller.signal;
								const response = await fetch(task.url, requestInit);

								const body = await response.body;
								reader = body.getReader();

								while (true) {
									const { done, value } = await reader.read();
									if (value) ws.send(value);  // don't send back "undefined" string when received nothing
									if (done) break;
								}
							} finally {
								upstreamGetCount -= 1;
								console.log("Dial GET DONE, remaining: ", upstreamGetCount);
								ws.close();
							}
						})();
						break;
					}
					case "POST": {
						upstreamPostCount += 1;

						const requestInit = prepareRequestInit(task.extra);
						requestInit.method = "POST";

						console.log("Dial POST", task.url);
						ws.send("ok");
						ws.onmessage = async (event) => {
							try {
								requestInit.body = event.data;
								const response = await fetch(task.url, requestInit);
								if (response.ok) {
									ws.send("ok");
								} else {
									console.error("bad status code");
									ws.send("fail");
								}
							} finally {
								upstreamPostCount -= 1;
								console.log("Dial POST DONE, remaining: ", upstreamPostCount);
								ws.close();
							}
						};
						break;
					}
				}

				check();
			};
			ws.onerror = function (event) {
				ws.close();
			};
		};
		let checkTask = setInterval(check, 1000);
	</script>
</body>
</html>
